#!python

# Copyright (C) 2008 David Golightly

# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

'''
cssx.py: syntax-enhancing compiler for CSSx
Outputs valid CSS2

Usage:

enhance.py -i <cssfile> [<outfile>]

Outputs to STDOUT if no outfile is given.

Options:
    -i  Ignore errors


Example Syntax:

$red = #f00;
$block = {
    border-width: 1px;
    padding: 10px;
}

.warning {
    color: $red;
}

.content {
    .warning {
        width: 200px;
    }

    p {
        font-size: 1.1em;
    }

    .header {
        @rule($block);
    }

    font-family: Verdana, Arial, sans-serif;
}
'''

import re, sys


class CSSParseException(Exception):
    pass


class CSSBindingException(Exception):
    pass



class CSSEnhance:
    variables = {}
    tree = []

    def __init__(self, csstext=None, options=[]):
        self.options = options
        if csstext != None:
            self.parse(csstext, options)

    def parse(self, csstext, options=None):
        if options == None:
            options = self.options

        if type(csstext) == file:
            csstext = csstext.read()

        p = self.CSSParser(csstext, options)
        self.variables = p.variables
        self.tree = p.tree


    def __str__(self):
        output = ''
        for node in self.tree:
            output += str(node) + '\n'
        return output


    def prepareForRender(self):
        renderTree = []

        for node in self.tree:
            if isinstance(node, Selector):
                renderTree += node.prepareForRender(self.variables)

        return renderTree


    def render(self):
        output = []

        renderTree = self.prepareForRender()

        for node in renderTree:
            if isinstance(node, Comment) or isinstance(node, Selector):
                output.append(node.render(self.variables, self.options))
        return '\n'.join(output)


    class CSSParser:
        # Select a comment and any trailing whitespace
        reCOMMENT = re.compile('/\*.*?\*/\s*')

        def __init__(self, csstext, options=[]):
            self.options = options

            self.csstext = csstext
            self.pos = 0
            self.tree = []
            self.variables = {}
            self.cssfile()


        def cssfile(self):
            self.csstext.lstrip()
            while self.pos < len(self.csstext)-1:
                self.getnext()


        def eatWhite(self):
            while self.pos < len(self.csstext) and self.csstext[self.pos].isspace():
                self.pos += 1


        def getnext(self):
            self.eatWhite()
            upcoming = self.upcoming()
            if upcoming.startswith('/*'):
                self.tree.append(self.comment())
            elif upcoming.startswith('$'):
                var = self.variable()
                self.variables[var.name] = var.value
                self.tree.append(var)
            else:
                self.tree.append(self.selector())


        def getLowest(self, *args):
            lowest = max(*args)
            for arg in args:
                if arg <= lowest and arg > -1:
                    lowest = arg
            return lowest


        def getLineNumber(self):
            return self.csstext[:self.pos].count('\n')+1


        def expect(self, what, nowhite=False):
            if not nowhite: self.eatWhite()
            if not self.upcoming().startswith(what):
                raise CSSParseException('[line %d] Expected "%s", got "%s"' % (self.getLineNumber(), what, self.upcoming()[:self.upcoming().find('\n')]))

            self.pos += len(what)
            return self.csstext[self.pos-len(what):self.pos]


        def upcoming(self):
            self.eatWhite()
            return self.csstext[self.pos:]


        def getNextTokenIndex(self, token, name=''):
            '''Finds and returns the next instance of a given token in the input string
            Doesn't advance the pointer'''
            if name == '':
                name = token
            idx = self.upcoming().find(token)
            if idx == -1:
                raise CSSParseException('[line %d] Expected "%s", got "%s"' % (self.getLineNumber(), name, self.upcoming()[:self.upcoming().find('\n')]))
            return idx


        def getToken(self, endChar):
            '''Returns the next token and advances the pointer'''
            idx = self.getNextTokenIndex(endChar)
            token = self.upcoming()[:idx].strip()
            self.pos += idx

            token = self.reCOMMENT.sub('', token)
            return token.strip()


        def comment(self):
            commentText = self.expect('/*')
            while not self.upcoming().startswith('*/'):
                commentText += self.csstext[self.pos]
                self.pos += 1
            commentText += self.expect('*/')
            self.eatWhite()
            return Comment(commentText, self.getLineNumber())


        def selector(self):
            selectorText = self.getToken('{')
            block = self.block()
            return Selector(selectorText, block, self.getLineNumber())


        def variable(self):
            value = None
            self.expect('$')
            name = self.getToken('=')
            self.expect('=')

            upcoming = self.upcoming()

            if upcoming.find('{') < upcoming.find(';'):
                value = self.block()
            else:
                value = self.getToken(';')
                self.expect(';')
            return VariableDeclaration(name, value, self.getLineNumber())


        def block(self):
            self.expect('{')
     
            children = []
            while True:
                upcoming = self.upcoming()
                nextBlock = upcoming.find('{')
                nextRule = upcoming.find(':')
                nextImport = upcoming.find('@rule')
                nextComment = upcoming.find('/*')
                endBlock = upcoming.find('}')

                # take the nearest of the above,
                # as long as it's not greater than endBlock
                # and is greater than -1
                next = self.getLowest(nextBlock, nextRule, nextImport, nextComment, endBlock) 

                if next == nextBlock:
                    children.append(self.selector())
                elif next == nextRule:
                    children.append(self.rule())
                elif next == nextImport:
                    children.append(self.ruleImport())
                elif next == nextComment:
                    children.append(self.comment())
                else:
                    break
                self.eatWhite()
            self.expect('}')
            return Block(children, self.getLineNumber())


        def rule(self):
            name = ''
            value = ''

            name = self.getToken(':')
            self.expect(':')
            value = self.getToken(';')
            self.expect(';')

            return Rule(name, value, self.getLineNumber())


        def ruleImport(self):
            self.expect('@rule(')
            name = self.getToken(')')
            self.expect(')')
            self.expect(';')
            return RuleImport(name, self.getLineNumber())



class Comment:
    def __init__(self, text, lineNumber=0):
        self.text = text
        self.lineNumber = lineNumber

    def __str__(self):
        return self.text

    def render(self, variables={}, options=[]):
        return self.text



class Block:
    def __init__(self, children, lineNumber=0):
        self.children = children
        self.lineNumber = lineNumber

    def __str__(self):
        outstr = []
        for child in self.children:
            outstr += str(child)
        return '{'+'\n'.join(outstr)+'}'

    def expandVariables(self, variables):
        i = 0
        while i in range(len(self.children)):
            node = self.children[i]
            if isinstance(node, Selector):
                node.prepareForRender(variables)
            elif isinstance(node, RuleImport):
                if node.ruleName[1:] in variables:
                    # replace the RuleImport instance with the variable's values
                    self.children[i:i+1] = variables[node.ruleName[1:]].children[:]
                    i -= 1 # run prepareForRender on the newly replaced node
                else:
                    raise CSSBindingException('Reference to unbound variable "%s" at line %d' % (node.ruleName, node.lineNumber))
            elif isinstance(node, Rule):
                node.expandTokens(variables)
            elif isinstance(node, VariableDeclaration):
                variables[node.name] = node.value
            i += 1


    def render(self, variables, options=[]):
        return '{\n'+self.renderChildren(variables, options)+'\n}'

    def renderChildren(self, variables, options=[]):
        return '\n'.join(child.render(variables, options) for child in self.children if not isinstance(child, Selector))

    def hasSelectors(self):
        for child in self.children:
            if isinstance(child, Selector):
                return True
        return False

    def getSelectors(self):
        return [child for child in self.children if isinstance(child, Selector)]



class Rule:
    reTOKEN = re.compile('\$(\w+)')

    def __init__(self, property, value, lineNumber=0):
        self.property = property.strip()
        self.value = value.strip()
        self.tokens = self.value.split(' ')
        self.lineNumber = lineNumber

    def __str__(self):
        return '    %s: %s;' % (self.property, self.value)

    def expandTokens(self, variables):
        matchedVariables = set([])
        noMoreVariables = False

        while not noMoreVariables:
            noMoreVariables = True
            for i in range(len(self.tokens)):
                token = self.tokens[i]
                match = self.reTOKEN.match(token)

                if match:
                    noMoreVariables = False
                    name = match.groups(0)[0]
                    if not name in variables:
                        raise CSSBindingException('Reference to unbound variable "%s" at line %d' % (name, self.lineNumber))
                    if name in matchedVariables:
                        raise CSSBindingException('Circular variable expansion of "%s" detected  at line %d' % (name, self.lineNumber))
                    self.tokens[i] = self.tokens[i].replace(match.group(), variables[name])
                    matchedVariables.add(name)

        self.value = ' '.join(self.tokens)


    def render(self, variables, options=[]):
        value = self.value
        # print 'rendering rule %s' % str(self)
        if self.value.startswith('$'):
            if not variables.has_key(self.value[1:]):
                raise CSSBindingException('Reference to unbound variable "%s" at line %d' % (self.value, self.lineNumber))
            value = variables[self.value[1:]]
        return '    %s: %s;' % (self.property, value)



class RuleImport:
    def __init__(self, ruleName, lineNumber=0):
        self.ruleName = ruleName
        self.lineNumber = lineNumber

    def __str__(self):
        return '    @rule(%s);' % (self.ruleName)

    def render(self, variables, options=[]):
        if not variables.has_key(self.ruleName[1:]):
            raise CSSBindingException('Reference to unbound variable "%s" at line %d' % (self.value, self.lineNumber))
        value = variables[self.ruleName[1:]]
        # print 'rendering ruleimport %s' % str(self)
        # print value
        outstr = str(value)
        if isinstance(value, Block):
            outstr = value.renderChildren(variables, options)
        return outstr.rstrip()



class Selector:
    reCOMMA = re.compile(',\s')
    reSPACE = re.compile('\s\s+')

    def __init__(self, name, block, lineNumber=0):
        self.name = self.reCOMMA.sub(',\n', self.reSPACE.sub(' ', name))
        self.block = block
        self.lineNumber = 0

    def __str__(self):
        return '%s %s\n' % (self.name, str(self.block))

    def render(self, variables, options=[]):
        return '%s %s\n' % (self.name, self.renderChildren(variables, options)) 

    def flattenNestedSelectors(self, selHead='', depth=0):
        '''Flatten selectors recursively nested in this selector's block
    so we can render all of them as top-level selectors'''
        flattenedSelectors = []
        for sel in self.block.getSelectors():
            newName = '%s %s' % (selHead, sel.name)
            newSel = Selector(newName, Block(sel.block.children[:]), sel.lineNumber)
            flattenedSelectors.append(newSel)

            flattenedSelectors += [Selector(s.name, s.block, s.lineNumber) for s in newSel.flattenNestedSelectors(newName, depth+1) if isinstance(s, Selector)]

        return flattenedSelectors

    def renderChildren(self, variables, options=[]):
        children = self.block.render(variables, options)
        return children

    def prepareForRender(self, variables):
        self.block.expandVariables(variables)

        # clone this selector
        children = [r for r in self.block.children if isinstance(r, Rule)]
        flattenedSelectors = [Selector(self.name, Block(children), self.lineNumber)]

        flattenedSelectors.extend(self.flattenNestedSelectors(self.name))
        return flattenedSelectors


class VariableDeclaration:
    def __init__(self, name, value, lineNumber=0):
        self.name = name
        self.value = value
        self.lineNumber = lineNumber

    def __str__(self):
        return '$%s = %s;' % (self.name, self.value)

    def render(self, variables, options=[]):
        return str(self.value)



if __name__ == '__main__':
    options = []
    infile = ''
    outfile = ''
    for arg in sys.argv[1:]:
        if not infile and arg[0:2] == '--':
            options.append(arg)
        elif not infile and arg[0] == '-':
            # break each cluster of options into a 
            # list of its constituent characters
            options.extend(c for c in arg[1:])
        elif not infile:
            infile = arg
        else:
            outfile = arg

    if not infile:
        print 'ERROR: no input file specified'
        print __doc__
        sys.exit(1)

    # print 'infile: "%s"' % infile
    enhance = None
    rendered = ''
    try:
        enhance = CSSEnhance(open(infile, 'r'), options)
        rendered = enhance.render()
    except CSSParseException, cssex:
        print 'ERROR: Syntax error: %s' % cssex
        sys.exit(1)
    except CSSBindingException, cssbx:
        print 'ERROR: %s' % cssbx
        print
        sys.exit(1)

    if not outfile:
        if infile.find('.cssx') != -1:
            outfile = infile.replace('.cssx', '.css')


    if outfile:
        out = open(outfile, 'w')
        out.write(rendered)
    elif '--parse' not in options:
        print rendered
    else:
        print '%s: parse ok' % infile

    sys.exit(0)

def test():
    return CSSEnhance(open('test.cssx'))
